package xyz.tanxiao.links.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import xyz.tanxiao.links.common.entity.BatchTaskResult;
import xyz.tanxiao.links.common.entity.BatchToShortRequest;
import xyz.tanxiao.links.common.entity.ToShortLinkRequest;
import xyz.tanxiao.links.common.enums.*;
import xyz.tanxiao.links.common.exception.ToShortLinkException;
import xyz.tanxiao.links.common.listener.BatchTaskEvent;
import xyz.tanxiao.links.common.utils.BatchTaskUtils;
import xyz.tanxiao.links.common.utils.NetworkUtils;
import xyz.tanxiao.links.config.GlobalProperties;
import xyz.tanxiao.links.core.cache.LayeringCacheOperations;
import xyz.tanxiao.links.core.mapping.LongLinkMappingKey;
import xyz.tanxiao.links.entity.SlDomainManage;
import xyz.tanxiao.links.entity.SlLinkMapping;
import xyz.tanxiao.links.mapper.SlLinkMappingMapper;
import xyz.tanxiao.links.service.SlDomainManageService;
import xyz.tanxiao.links.service.SlLinkMappingService;

import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.stream.Collectors;

@Slf4j
@RequiredArgsConstructor
@Service
public class SlLinkMappingServiceImpl extends ServiceImpl<SlLinkMappingMapper, SlLinkMapping> implements SlLinkMappingService {

	private final ThreadPoolTaskExecutor taskExecutor;

	private final RedisTemplate<String, Object> redisTemplate;

	private final ApplicationEventPublisher eventPublisher;

	private final LayeringCacheOperations layeringCacheOperations;

	private final LongLinkMappingKey<String> linkMappingKey;

	private final SlDomainManageService domainManageService;

	private final GlobalProperties globalProperties;

	private static final Set<String> MAPPING_KEYS_TEMP_CACHE = Collections.synchronizedSet(new HashSet<>(30));


	/**
	 * 长链接转短链接
	 *
	 * @param request 请求参数
	 * @return 短链接
	 */
	@Transactional(rollbackFor = Exception.class)
	@Override
	public String toShortLink(ToShortLinkRequest request) {
		String sourceUrl = request.getSourceUrl();
		if (!(sourceUrl.startsWith("http://") || sourceUrl.startsWith("https://"))) {
			throw new ToShortLinkException("生成失败，请输入 http:// 或 https:// 开头的链接。");
		}

		// 检查转换的主机域名
		this.checkSourceDomain(sourceUrl);
		// 短链域名
		String domainName = this.globalProperties.getDomainName();
		// 生成一个不重复的映射Key
		String mappingKey = getNoRepeatMappingKey(sourceUrl);

		// 封装长链接映射信息
		SlLinkMapping newMapping = new SlLinkMapping();
		newMapping.setMappingKey(mappingKey);
		newMapping.setDomainName(domainName);
		newMapping.setLongLink(sourceUrl);
		newMapping.setStatus(MappingKeyStatusEnum.NORMAL.getStatus());
		newMapping.setEnableTime(request.getEnableTime());
		newMapping.setDisableTime(request.getDisableTime());
		newMapping.setAllowTotal(request.getAllowTotal());
		newMapping.setAccessPassword(request.getAccessPassword());
		newMapping.setMappingWay(MappingWayEnum.MD5_BASE62.getMappingWay());
		newMapping.setCreateTime(LocalDateTime.now());
		newMapping.setRequestIp(NetworkUtils.getRequestIp());
		this.save(newMapping);

		// 映射信息添加Redis缓存
		String cacheKey = LinksCacheKeyEnum.SHORT_LINKS_CACHE_KEY.getCacheKey() + mappingKey;
		this.layeringCacheOperations.set(cacheKey, newMapping);
		return domainName + mappingKey;
	}


	/**
	 * 批量长链接转短链接
	 *
	 * @param request 请求参数
	 * @return 短链接
	 */
	@Override
	public String batchToShortLink(BatchToShortRequest request) {
		// 导入文件检查
		MultipartFile batchFile = request.getBatchFile();
		BatchTaskUtils.checkFile(batchFile, this.globalProperties.getBatchMaxFileSize());
		List<String> longLinks = BatchTaskUtils.readFile(batchFile);

		// 长链数量检查
		int maxLongLinks = this.globalProperties.getBatchMaxLongLinks();
		if (maxLongLinks > 0 && longLinks.size() > maxLongLinks) {
			throw new ToShortLinkException("短链批量生成失败，单次导入的长链接数量不能超过 " + maxLongLinks + " 条。");
		}

		// 短链数量检查
		int maxMappingKeys = this.globalProperties.getBatchMaxMappingKeys();
		if (maxMappingKeys > 0 && request.getBatchSize() > maxMappingKeys) {
			throw new ToShortLinkException("短链批量生成失败，短链批量生成数量最大允许为 " + maxMappingKeys + " 条。");
		}
		// 批量生成短链
		return this.startBatchTask(request, longLinks);
	}


	/**
	 * 定时查询短链批量生成结果
	 *
	 * @param taskId 任务标识
	 * @return 处理结果 (文件下载地址)
	 */
	@Override
	public BatchTaskEvent scheduledQueryResult(String taskId) {
		String cacheKey = LinksCacheKeyEnum.BATCH_TO_LINK_TASK_CACHE_KEY.getCacheKey() + taskId;
		return (BatchTaskEvent) this.redisTemplate.opsForValue().get(cacheKey);
	}


	/**
	 * 生成一个不重复的映射Key
	 *
	 * @param longLink 长链接
	 * @return 映射Key
	 */
	@Override
	public String getNoRepeatMappingKey(String longLink) {
		for (String mappingKey : this.linkMappingKey.toMappingKey(longLink)) {
			long mappingKeyCount = this.count(
				Wrappers.lambdaQuery(SlLinkMapping.class).eq(SlLinkMapping::getMappingKey, mappingKey)
			);
			if (mappingKeyCount < 1) {
				return mappingKey;
			}
		}
		return this.getNoRepeatMappingKey(longLink);
	}


	/**
	 * 生成指定数量不重复的映射Key
	 *
	 * @param longLink 长链接
	 * @param batchSize 短链接生成数量
	 * @return 短链接集合
	 */
	@Override
	public Set<String> getNoRepeatMappingKeys(String longLink, int batchSize) {
		// 生成映射Key (重复率7/10000左右)
		int i = batchSize / 4 + 1;
		Set<String> newMappingKeys = new HashSet<>(batchSize);
		for (int j = 0; j < i; j++) {
			List<String> mappingKeys = this.linkMappingKey.toMappingKey(longLink);
			if (batchSize > 1) {
				newMappingKeys.addAll(mappingKeys);
			} else {
				newMappingKeys.add(mappingKeys.get(0));
			}
		}

		// 查询数据库中的映射Key，移除重复的映射Key。
		List<String> repeatMappingKeys = this.getRepeatMappingKeys(newMappingKeys);
		if (!repeatMappingKeys.isEmpty()) {
			repeatMappingKeys.forEach(newMappingKeys::remove);
		}

		// 检查重复映射Key容器，移除重复的映射Key。
		if (!MAPPING_KEYS_TEMP_CACHE.isEmpty()) {
			newMappingKeys.removeAll(MAPPING_KEYS_TEMP_CACHE);
		}

		// 生成的映射Keys数量错误全部重复
		if (newMappingKeys.isEmpty()) {
			return this.getNoRepeatMappingKeys(longLink, batchSize);
		}

		// 生成的映射Keys数量超出
		int newMappingSize = newMappingKeys.size();
		if (newMappingSize > batchSize) {
			Set<String> mappingKeys = new LinkedHashSet<>(CollUtil.sub(newMappingKeys, 0, batchSize));
			MAPPING_KEYS_TEMP_CACHE.addAll(mappingKeys);
			return mappingKeys;
		}

		// 生成的映射Keys数量错误部分重复
		int lessMappingKeys = batchSize - newMappingSize;
		if (lessMappingKeys > 0) {
			for (int j = 0; j < lessMappingKeys; j++) {
				newMappingKeys.add(this.getNoRepeatMappingKey(longLink));
			}
		}

		MAPPING_KEYS_TEMP_CACHE.addAll(newMappingKeys);
		return newMappingKeys;
	}


	/**
	 * 通过新映射Key查询重复的映射Key
	 *
	 * @param newMappingKeys 新映射Key
	 * @return 重复的映射Key
	 */
	@Override
	public List<String> getRepeatMappingKeys(Set<String> newMappingKeys) {
		return this.listObjs(
			Wrappers.lambdaQuery(SlLinkMapping.class)
				.select(SlLinkMapping::getMappingKey)
				.in(SlLinkMapping::getMappingKey, newMappingKeys),
			Object::toString
		);
	}


	/**
	 * 检查转换的主机域名
	 *
	 * @param sourceUrl 原始URL
	 */
	@Override
	public void checkSourceDomain(String sourceUrl) {
		if (!this.globalProperties.isEnableCheckDomain()) {
			return;
		}
		// 域名检查
		List<SlDomainManage> domains = this.domainManageService.getDomainsByStatus(CommonStatusEnum.ENABLE);
		domains.forEach(domain -> {
			if (sourceUrl.startsWith(domain.getDomainName())) {
				if (DomainTypeEnum.PROTECTION.getType() == domain.getType()) {
					throw new ToShortLinkException("生成失败，此链接域名为系统保护域名。");
				} else {
					throw new ToShortLinkException("生成失败，此链接域名已被禁用无法转换。");
				}
			}
		});
	}


	/**
	 * 开始批量处理任务
	 *
	 * @param request request 请求参数
	 * @param longLinks 长链接
	 */
	private String startBatchTask(BatchToShortRequest request, List<String> longLinks) {
		// 批量任务标识
		String taskId = UUID.randomUUID().toString();
		this.eventPublisher.publishEvent(new BatchTaskEvent(taskId, 0));

		this.taskExecutor.execute(() -> {
			try {
				// 批量生成结果
				List<BatchTaskResult> taskResults = this.buildTaskResults(request, longLinks);
				this.eventPublisher.publishEvent(new BatchTaskEvent(taskId, 1));

				// 保存映射数据
				this.batchTaskSaveMappingKeys(taskResults, this.globalProperties.getDomainName());
				this.eventPublisher.publishEvent(new BatchTaskEvent(taskId, 2));

				// 写入短链数据
				String pattern = this.globalProperties.getStaticPathPattern();
				String locations = this.globalProperties.getStaticLocations();
				String downloadUrl = pattern + BatchTaskUtils.writeFile(taskId, taskResults, locations);
				this.eventPublisher.publishEvent(new BatchTaskEvent(taskId, 3,  downloadUrl));
				log.info("=> 短链批量生成完毕");

			} catch (Exception e) {
				this.eventPublisher.publishEvent(new BatchTaskEvent(taskId, 4));
				Thread.currentThread().interrupt();
			}
		});
		return taskId;
	}


	/**
	 * 构建批量生成任务处理结果
	 *
	 * @param request 请求参数
	 * @param longLinks 长链接列表
	 * @return 任务处理结果
	 */
	private List<BatchTaskResult> buildTaskResults(BatchToShortRequest request, List<String> longLinks) throws InterruptedException {
		// 长链数据切片
		List<List<String>> splitLongLinks = ListUtil.splitAvg(longLinks, 5);
		List<BatchTaskResult> taskResults = Collections.synchronizedList(new ArrayList<>());
		CountDownLatch downLatch = new CountDownLatch(splitLongLinks.size());

		for (List<String> splitLongLink : splitLongLinks) {
			this.taskExecutor.execute(() -> {
				splitLongLink.forEach(longLink -> {
					// 构建批量任务结果
					BatchTaskResult taskResult = new BatchTaskResult();
					BeanUtil.copyProperties(request, taskResult);
					taskResult.setLongLink(longLink);
					taskResult.setResult(1);
					taskResult.setResultInfo("批量生成成功");
					try {
						this.checkSourceDomain(longLink);
						Set<String> mappingKeys = this.getNoRepeatMappingKeys(longLink, request.getBatchSize());
						taskResult.setMappingKeys(mappingKeys);
					} catch (Exception e) {
						taskResult.setResult(0);
						taskResult.setResultInfo("批量生成失败：" + e.getMessage());
					}
					taskResults.add(taskResult);
				});
				downLatch.countDown();
			});
		}

		downLatch.await();
		// 清除映射Keys重复检查的临时缓存
		MAPPING_KEYS_TEMP_CACHE.clear();
		return taskResults;
	}


	/**
	 * 保存任务映射数据 (批量插入)
	 *
	 * @param taskResults 任务处理结果
	 * @param domainName 短链域名
	 */
	private void batchTaskSaveMappingKeys(List<BatchTaskResult> taskResults, String domainName) throws InterruptedException {
		// 过滤批量生成成功的任务数据
		List<BatchTaskResult> results = taskResults.stream().filter(t -> t.getResult() == 1)
			.collect(Collectors.toList());

		// 构建映射数据
		List<SlLinkMapping> mappingKeys = new ArrayList<>(30);
		results.forEach(result -> {
			Set<String> shortLinks = result.getShortLinks();
			for (String mappingKey : result.getMappingKeys()) {
				SlLinkMapping mapping = new SlLinkMapping();
				BeanUtil.copyProperties(result, mapping);
				mapping.setMappingKey(mappingKey);
				mapping.setDomainName(domainName);
				mapping.setStatus(0);
				mapping.setMappingWay(MappingWayEnum.MD5_BASE62.getMappingWay());
				mapping.setCreateTime(LocalDateTime.now());
				mappingKeys.add(mapping);
				shortLinks.add(domainName + mappingKey);
			}
			result.setTotal(shortLinks.size());
		});

		// 映射数据切片
		List<List<SlLinkMapping>> splitMappingKeys = ListUtil.splitAvg(mappingKeys, 3);
		CountDownLatch downLatch = new CountDownLatch(splitMappingKeys.size());

		// 多线程批量插入数据
		for (List<SlLinkMapping> splitMappingKey : splitMappingKeys) {
			this.taskExecutor.execute(() -> {
				super.saveBatch(splitMappingKey, 1500);
				downLatch.countDown();
			});
		}
		downLatch.await();
	}

}
